﻿#if UNITY_2019_4_OR_NEWER
#define UNITY_3D
using UnityEngine;
#endif

using System.Collections.Generic;
using System.IO;
using System.Text;

namespace Devil
{
#if UNITY_3D
    [System.AttributeUsage(System.AttributeTargets.Class | System.AttributeTargets.Method, AllowMultiple = true)]
    public class PlatformAttribute : System.Attribute
    {
        public int Priority { get; set; }
        public RuntimePlatform platform { get; private set; }
        public PlatformAttribute(RuntimePlatform plat)
        {
            platform = plat;
        }
    }
#endif
    public abstract class NativeFileSystem
    {
        #region file system instance

        static NativeFileSystem sFileSystem;
        static readonly object _lock = new object();

        public static NativeFileSystem Instance => sFileSystem;

        public static void SetFileSystem(NativeFileSystem system)
        {
            lock (_lock)
            {
                var sys = sFileSystem;
                if (sys == system)
                    return;
                sFileSystem = system;

                if (sys != null)
                {
                    sys.OnDestroy();
                    Debug.LogFormat("File System \"{0}({1:x})\" was destroied", sys.GetType().Name, sys.GetHashCode());
                }

                if (sFileSystem != null)
                {
                    sFileSystem.OnBecomeActive();
                    Debug.LogFormat("File System \"{0}({1:x})\" was created", sFileSystem.GetType().Name, sFileSystem.GetHashCode());
                }
            }
        }

        public static NativeFileSystem CreateFileSystem(RuntimePlatform platform)
        {
            try
            {
                var basetp = typeof(NativeFileSystem);
                System.Reflection.Assembly[] assemblies = System.AppDomain.CurrentDomain.GetAssemblies();
                var notype = new System.Type[0];
                System.Type fsType = null;
                int priority = 0;
                foreach (var assembly in assemblies)
                {
                    var tps = assembly.GetTypes();
                    foreach (var tp in tps)
                    {
                        if (tp.IsAbstract || !tp.IsSubclassOf(basetp))
                            continue;
                        var att = tp.GetCustomAttributes(typeof(PlatformAttribute), true);
                        bool use = att == null || att.Length == 0;
                        int pri = 0;
                        if (!use)
                        {
                            for (int i = 0; i < att.Length; i++)
                            {
                                if (((PlatformAttribute)att[i]).platform == platform)
                                {
                                    use = true;
                                    pri = ((PlatformAttribute)att[i]).Priority;
                                    break;
                                }
                            }
                        }
                        else
                        {
                            use = fsType == null;
                        }
                        if (use && (fsType == null || priority < pri))
                        {
                            fsType = tp;
                            priority = pri;
                        }
                    }
                }
                var cons = fsType == null ? null : fsType.GetConstructor(notype);
                if (cons == null)
                    return null;
                var sys = cons.Invoke(null) as NativeFileSystem;
                return sys;
            }
            catch (System.Exception e)
            {
                Debug.LogException(e);
                return null;
            }
        }

        static NativeFileSystem EnsureFileSystem()
        {
#if UNITY_EDITOR
            if (sFileSystem == null && !ParallelUnity.isPlaying)
            {
                SetFileSystem(CreateFileSystem(ParallelUnity.platform));
            }
#endif
            if (sFileSystem == null)
#if UNITY_EDITOR
                throw new IOException(string.Format("Don't find native file system for {0}.", ParallelUnity.platform));
#else
                throw new IOException("Don't find native file system for current platform.");
#endif
            return sFileSystem;
        }

        #endregion

        #region file operations 

        /// <summary>
        /// 确保文件夹存在，如果不存在则创建
        /// </summary>
        /// <param name="folder"></param>
        public static void EnsureDirectory(string folder)
        {
            if (string.IsNullOrEmpty(folder))
                return;
            var sys = EnsureFileSystem();
            if (!sys.NativeExistDirectory(folder))
                sys.NativeMakeDirectory(folder);
        }

        /// <summary>
        /// 是否存在文件夹
        /// </summary>
        /// <param name="folder"></param>
        /// <returns></returns>
        public static bool HasFolder(string folder)
        {
            return EnsureFileSystem().NativeExistDirectory(folder);
        }

        /// <summary>
        /// 是否存在文件
        /// </summary>
        /// <param name="path"></param>
        /// <returns></returns>
        public static bool HasFile(string path)
        {
            if (string.IsNullOrEmpty(path))
                return false;
            return EnsureFileSystem().NativeExistFile(path);
        }

        /// <summary>
        /// 删除文件
        /// </summary>
        /// <param name="path"></param>
        public static void DeleteFile(string path)
        {
            EnsureFileSystem().NativeDeleteFile(path);
        }

        /// <summary>
        /// 移动文件
        /// </summary>
        /// <param name="from"></param>
        /// <param name="to"></param>
        public static void MoveFile(string from, string to)
        {
            EnsureFileSystem().NativeMoveFile(from, to);
        }

        /// <summary>
        /// 复制文件
        /// </summary>
        /// <param name="from"></param>
        /// <param name="to"></param>
        public static void CopyFile(string from, string to)
        {
            EnsureFileSystem().NativeCopyFile(from, to);
        }

        /// <summary>
        /// 获取路径中的文件名
        /// </summary>
        /// <param name="path"></param>
        /// <param name="excludeExtension"></param>
        /// <returns></returns>
        public static string GetFileName(string path, bool excludeExtension)
        {
            return EnsureFileSystem().NativeGetFileName(path, excludeExtension);
        }

        /// <summary>
        /// 获取路径中的文件夹名
        /// </summary>
        /// <param name="path"></param>
        /// <returns></returns>
        public static string GetFolderName(string path)
        {
            return EnsureFileSystem().NativeGetFolderName(path);
        }

        public static string GetRootFolder(string path)
        {
            return EnsureFileSystem().NativeGetRootFolder(path);
        }

        public static string GetSubFile(string path)
        {
            return EnsureFileSystem().NativeGetSubFile(path);
        }

        /// <summary>
        /// 替换路径中的文件名
        /// </summary>
        /// <param name="path"></param>
        /// <param name="fileName"></param>
        /// <returns></returns>
        public static string ReplaceFileName(string path, string fileName)
        {
            return EnsureFileSystem().NativeReplaceFileName(path, fileName);
        }

        /// <summary>
        /// 组合文件路径
        /// </summary>
        /// <param name="path1"></param>
        /// <param name="path2"></param>
        /// <returns></returns>
        public static string Combine(string path1, string path2)
        {
            if (string.IsNullOrEmpty(path2))
                return path1;
            if (string.IsNullOrEmpty(path1))
                return path2;
            return EnsureFileSystem().CombinePath(path1, path2);
        }

        /// <summary>
        /// 组合文件路径
        /// </summary>
        /// <param name="path1"></param>
        /// <param name="path2"></param>
        /// <param name="pathes"></param>
        /// <returns></returns>
        public static string Combine(string path1, string path2, params string[] pathes)
        {
            return EnsureFileSystem().CombinePath(path1, path2, pathes);
        }

        /// <summary>
        /// 获取子文件在父文件夹中的相对路径
        /// </summary>
        /// <param name="basePath"></param>
        /// <param name="subPath"></param>
        /// <returns></returns>
        public static string CalculateRelativePath(string basePath, string subPath)
        {
            return EnsureFileSystem().NativeCalculateRelativePath(basePath, subPath);
        }

        /// <summary>
        /// 列出文件夹中的所有文件
        /// </summary>
        /// <param name="folder"></param>
        /// <param name="recursive"></param>
        /// <param name="filter"></param>
        /// <param name="files"></param>
        public static void ListFiles(string folder, bool recursive, FilterDelegate<string> filter, ICollection<string> files)
        {
            EnsureFileSystem().GetFiles(folder, recursive, filter, files);
        }

        /// <summary>
        /// 列出文件夹中的所有文件夹
        /// </summary>
        /// <param name="folder"></param>
        /// <param name="recursive"></param>
        /// <param name="filter"></param>
        /// <param name="subFolders"></param>
        public static void ListFolders(string folder, bool recursive, FilterDelegate<string> filter, ICollection<string> subFolders)
        {
            EnsureFileSystem().GetSubFolders(folder, recursive, filter, subFolders);
        }

        /// <summary>
        /// 打开文件流
        /// </summary>
        /// <param name="path"></param>
        /// <param name="mode"></param>
        /// <param name="access"></param>
        /// <returns></returns>
        public static Stream OpenFile(string path, FileMode mode, FileAccess access)
        {
            var stream = EnsureFileSystem().GetFileStream(path, mode, access);
            return stream;
        }

        /// <summary>
        /// 追加文件输入
        /// </summary>
        /// <param name="path"></param>
        /// <param name="encoding"></param>
        /// <returns></returns>
        public static StreamWriter AppendText(string path, Encoding encoding)
        {
            var stream = EnsureFileSystem().GetFileStream(path, FileMode.Append, FileAccess.Write);
            return new StreamWriter(stream, encoding);
        }

        public static string ReadAllText(string path, bool logMsg = false)
        {
            return ReadAllText(path, Encoding.UTF8, logMsg);
        }

        public static string ReadAllText(string path, Encoding encoding, bool logMsg)
        {
            Stream stream = null;
            StreamReader reader = null;
            string str = null;
            try
            {
                stream = OpenFile(path, FileMode.Open, FileAccess.Read);
                reader = new StreamReader(stream, encoding);
                str = reader.ReadToEnd();
            }
            catch (System.Exception e)
            {
                if (logMsg)
                    Debug.LogException(e);
            }
            finally
            {
                if (reader != null)
                {
                    reader.Close();
                }
                if (stream != null)
                {
                    stream.Close();
                }
            }
            return str;
        }

        public static bool WriteAllText(string path, string text)
        {
            return WriteAllText(path, text, Encoding.UTF8);
        }

        public static bool WriteAllText(string path, string text, Encoding encoding)
        {
            bool success = true;
            Stream stream = null;
            StreamWriter writer = null;
            try
            {
                stream = OpenFile(path, FileMode.Create, FileAccess.Write);
                writer = new StreamWriter(stream, encoding);
                writer.Write(text);
                writer.Flush();
            }
            catch (System.Exception e)
            {
                Debug.LogException(e);
                success = false;
            }
            finally
            {
                if (writer != null)
                {
                    writer.Close();
                }
                if (stream != null)
                {
                    stream.Close();
                }
            }
            return success;
        }

        public static byte[] ReadAllBytes(string path, bool logMsg = false)
        {
            byte[] bytes;
            Stream stream = null;
            try
            {
                stream = OpenFile(path, FileMode.Open, FileAccess.Read);
                var len = stream.Length;
                if (len > int.MaxValue)
                    throw new IOException("read all bytes out of bounds.");
                bytes = new byte[(int)len];
                var size = bytes.Length < 512 ? bytes.Length : 512;
                int index = 0;
                int num;
                while (size > 0 && (num = stream.Read(bytes, index, size)) > 0)
                {
                    index += num;
                    size = bytes.Length - index;
                    if (size > 512)
                        size = 512;
                }
            }
            catch (System.Exception e)
            {
                if (logMsg)
                    Debug.LogException(e);
                bytes = null;
            }
            finally
            {
                if (stream != null)
                {
                    stream.Close();
                }
            }
            return bytes;
        }

        public static bool WriteAllBytes(string path, byte[] bytes)
        {
            Stream stream = null;
            bool success = true;
            try
            {
                stream = OpenFile(path, FileMode.Create, FileAccess.Write);
                stream.Write(bytes, 0, bytes.Length);
            }
            catch (System.Exception e)
            {
                Debug.LogException(e);
                success = false;
            }
            finally
            {
                if (stream != null)
                    stream.Close();
            }
            return success;
        }

        #endregion

        #region file system implemention

        protected byte[] mBuffer;
        ~NativeFileSystem()
        {
            if (sFileSystem == this)
            {
                sFileSystem = null;
                OnDestroy();
#if UNITY_EDITOR
                Debug.LogFormat("File System \"{0}({1:x})\" was destroied", GetType().Name, GetHashCode());
#endif
            }
        }

        protected abstract void OnBecomeActive();

        protected abstract void OnDestroy();

        protected abstract bool NativeExistFile(string path);

        protected abstract bool NativeExistDirectory(string path);

        protected abstract void NativeMakeDirectory(string path);

        protected abstract void NativeDeleteFile(string path);

        protected abstract void NativeMoveFile(string from, string to);

        protected abstract Stream GetFileStream(string path, FileMode mode, FileAccess access);

        protected virtual void NativeCopyFile(string from, string to)
        {
            if (mBuffer == null)
                mBuffer = new byte[2048];
            Stream ins = null, dest = null;
            bool failed = false;
            try
            {
                ins = GetFileStream(from, FileMode.Open, FileAccess.Read);
                dest = GetFileStream(to, FileMode.Create, FileAccess.Write);
                int len;
                while ((len = ins.Read(mBuffer, 0, mBuffer.Length)) > 0)
                {
                    dest.Write(mBuffer, 0, len);
                }
            }
            catch (System.Exception e)
            {
                Debug.LogException(e);
                failed = true;
            }
            finally
            {
                if (ins != null)
                {
                    ins.Close();
                    ins.Dispose();
                }
                if (dest != null)
                {
                    dest.Close();
                    dest.Dispose();
                }
            }
            if (failed)
                throw new IOException(string.Format("Failed to copy file from \"{0}\" to \"{1}\"", from, to));
        }

        protected virtual string NativeGetFileName(string path, bool excludeExtension)
        {
            var i = path.LastIndexOf('/');
#if UNITY_STANDALONE_WIN
            var n = path.LastIndexOf('\\');
            if (n > i)
                i = n;
#endif
            var ext = path.LastIndexOf('.');
            if (i > 0)
                return excludeExtension && ext > i ? path.Substring(i + 1, ext - i - 1) : path.Substring(i + 1);
            else
                return excludeExtension && ext > 0 ? path.Substring(0, ext) : path;
        }

        protected virtual string NativeGetFolderName(string path)
        {
            var i = path.LastIndexOf('/');
#if UNITY_STANDALONE_WIN
            var n = path.LastIndexOf('\\');
            if (n > i)
                i = n;
#endif
            return i > 0 ? path.Substring(0, i) : ".";
        }

        protected virtual string NativeGetRootFolder(string path)
        {
            var i = path.IndexOf('/');
#if UNITY_STANDALONE_WIN
            var n = path.IndexOf('\\');
            if (n > i)
                i = n;
#endif
            return i > 0 ? path.Substring(0, i) : "./";
        }

        protected virtual string NativeGetSubFile(string path)
        {
            var i = path.IndexOf('/');
#if UNITY_STANDALONE_WIN
            var n = path.IndexOf('\\');
            if (n > i)
                i = n;
#endif
            return i > 0 ? path.Substring(i + 1) : null;
        }


        protected virtual string NativeReplaceFileName(string path, string newFileName)
        {
            var i = path.LastIndexOf('/');
#if UNITY_STANDALONE_WIN
            var n = path.LastIndexOf('\\');
            if (n > i)
                i = n;
#endif
            var ext = path.LastIndexOf('.');
            if (i == -1)
                return ext == -1 ? newFileName : ParallelUtils.Concat(newFileName, path.Substring(ext));
            else if (ext == -1)
                return ParallelUtils.Concat(path.Substring(0, i + 1), newFileName);
            else
                return ParallelUtils.Concat(path.Substring(0, i + 1), newFileName, path.Substring(ext));
        }

        protected virtual string NativeCalculateRelativePath(string basePath, string subPath)
        {
            var p1 = Path.GetFullPath(basePath);
            var p2 = Path.GetFullPath(subPath);
            if (p1 == p2 || p1.Length >= p2.Length)
                return null;
            if (p2.StartsWithIgnoreCase(p1))
            {
                var last = p1[p1.Length - 1];
                var path = p2.Substring(p1.Length + (last == '/' || last == '\\' ? 0 : 1));
#if UNITY_STANDALONE_WIN
                path = path.Replace('\\', '/');
#endif
                return path;
            }
            else
                return null;
        }

        protected virtual string CombinePath(string path1, string path2)
        {
            var buf = ParallelUtils.GetBuilder();
            buf.Append(path1);
            var c = buf.Length > 0 ? buf[buf.Length - 1] : '/';
            var c2 = path2[0];
            if (c != '/' && c != '\\' && c2 != '/' && c2 != '\\')
                buf.Append('/');
            buf.Append(path2);
            return ParallelUtils.ReturnRelease(buf);
        }

        protected virtual string CombinePath(string path1, string path2, params string[] path)
        {
            var buf = ParallelUtils.GetBuilder();
            buf.Append(path1);
            var c = buf.Length > 0 ? buf[buf.Length - 1] : '/';
            var c2 = path2[0];
            if (c != '/' && c != '\\' && c2 != '/' && c2 != '\\')
                buf.Append('/');
            buf.Append(path2);
            for (int i = 0; i < path.Length; i++)
            {
                c = buf.Length > 0 ? buf[buf.Length - 1] : '/';
                c2 = path[i][0];
                if (c != '/' && c != '\\' && c2 != '/' && c2 != '\\')
                    buf.Append('/');
                buf.Append(path[i]);
            }
            return ParallelUtils.ReturnRelease(buf);
        }

        protected virtual void GetFiles(string rootFolder, bool recursive, FilterDelegate<string> filter, ICollection<string> files)
        {
            var dic = new DirectoryInfo(rootFolder);
            var info = dic.GetFiles("*", recursive ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly);
            var len = info == null ? 0 : info.Length;
            for (int i = 0; i < len; i++)
            {
                var path = info[i].FullName;
                if (filter == null || filter(path))
                    files.Add(path);
            }
        }

        protected virtual void GetSubFolders(string rootFolder, bool recursive, FilterDelegate<string> filter, ICollection<string> folders)
        {
            var dic = new DirectoryInfo(rootFolder);
            var info = dic.GetDirectories("*", recursive ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly);
            var len = info == null ? 0 : info.Length;
            for (int i = 0; i < len; i++)
            {
                var path = info[i].FullName;
                if (filter == null || filter(path))
                    folders.Add(path);
            }
        }

        #endregion
    }

}